using System;
using System.Collections.Generic;
#if(!WindowsCE)
using System.Linq.Expressions;
#endif
using System.Reflection;
using ValidationFramework.Extensions;
using System.Linq;

namespace ValidationFramework.Reflection
{
	//Design aims
	//-only one instance of TypeDescriptor should exists for each type
	/// <summary>
	/// A light-weight wrapper for <see cref="Type"/>.
	/// </summary>
#if (!SILVERLIGHT)
	[Serializable]
#endif
	public class TypeDescriptor : IPropertyRuleRepository
	{

		// --- Fields

		internal const BindingFlags PropertyFlags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic;


		// --- Constructors

		/// <summary>
		/// Initialize a new instance of the <see cref="TypeDescriptor"/> class.
		/// </summary>
		/// <param name="runtimeTypeHandle">The <see cref="System.RuntimeTypeHandle"/> for the <see cref="Type"/> to wrap.</param>
		/// <exception cref="ArgumentException"><paramref name="runtimeTypeHandle"/> represents an interface.</exception>
		internal TypeDescriptor(RuntimeTypeHandle runtimeTypeHandle)
		{
			var currentType = Type.GetTypeFromHandle(runtimeTypeHandle);
			if (currentType.IsInterface)
			{
				throw new ArgumentException(string.Format("Cannot create a TypeDescriptor for an interface. Interface = {0}'", currentType.ToUserFriendlyString()), "runtimeTypeHandle");
			}
			RuntimeTypeHandle = runtimeTypeHandle;

			Properties = new PropertyCollection(this);

			ProccessProperties(currentType);

			Properties.SetToReadOnly();
		}


		// --- Constructor | Helpers

		private struct PropertyLink
		{
			public PropertyDescriptor PropertyDescriptor { get; set; }
			public PropertyInfo PropertyInfo { get; set; }
		}

		private void ProccessProperties(Type currentType)
		{
			var interfaceMapping = GetInterfaceMapping(currentType);
			var dictionary = new Dictionary<string, PropertyLink>();
			foreach (var keyValuePair in interfaceMapping)
			{
				var propertyInfo = keyValuePair.Key;
				if (!propertyInfo.PropertyType.IsGenericParameter)
				{
					var getMethod = propertyInfo.GetGetMethod(true);
					if (getMethod != null)
					{
						var propertyDescriptor = new PropertyDescriptor(this, propertyInfo, getMethod);
						var propertyLink = new PropertyLink
											{
												PropertyInfo = propertyInfo,
												PropertyDescriptor = propertyDescriptor
											};
						dictionary.Add(propertyDescriptor.Name, propertyLink);
						foreach (var interfacePropertyInfo in keyValuePair.Value)
						{
							propertyDescriptor.AddStateRulesFromAttributes(interfacePropertyInfo);
							propertyDescriptor.AddValueRulesFromAttributes(interfacePropertyInfo);
						}
					}
				}
			}
			var staticPropertyInfoList = currentType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic);
			foreach (var propertyInfo in staticPropertyInfoList)
			{
				if (!propertyInfo.PropertyType.IsGenericParameter)
				{
					var getMethod = propertyInfo.GetGetMethod(true);
					if (getMethod != null)
					{
						var propertyDescriptor = new PropertyDescriptor(this, propertyInfo, getMethod);
						var propertyLink = new PropertyLink
											{
												PropertyInfo = propertyInfo,
												PropertyDescriptor = propertyDescriptor
											};
						dictionary.Add(propertyDescriptor.Name, propertyLink);
					}
				}
			}
			ProcessBaseClassProperties(dictionary, currentType);
			AddPropertiesWithRules(dictionary);
		}

		private void AddPropertiesWithRules(Dictionary<string, PropertyLink> properties)
		{
			foreach (var propertyLink in properties.Values)
			{
				var propertyInfo = propertyLink.PropertyInfo;
				var propertyDescriptor = propertyLink.PropertyDescriptor;
				if (propertyDescriptor.RuleCount > 0 || propertyInfo.ContainsAttribute<IncludeMemberAttribute>())
				{
					Properties.Add(propertyDescriptor);
				}
			}
		}
		
		private void ProcessBaseClassProperties(IDictionary<string, PropertyLink> dictionary, Type currentType)
		{
			var baseType = currentType.BaseType;
			if (baseType != null)
			{
				if (AssemblyCache.CanReflectOverAssembly(currentType.Assembly, baseType.Assembly))
				{
					var baseTypeDescriptor = TypeCache.GetType(baseType.TypeHandle);
					foreach (var basePropertyDescriptor in baseTypeDescriptor.Properties)
					{
						if (!basePropertyDescriptor.IsStatic)
						{
							PropertyDescriptor existingPropertyDescriptor;
							PropertyLink propertyLink;
							if (dictionary.TryGetValue(basePropertyDescriptor.Name, out propertyLink))
							{
								existingPropertyDescriptor = propertyLink.PropertyDescriptor;
							}
							else
							{
								var propertyInfo = baseType.GetProperty(basePropertyDescriptor.Name, PropertyFlags);
								var getMethodInfo = propertyInfo.GetGetMethod(true);
								existingPropertyDescriptor = new PropertyDescriptor(this, propertyInfo, getMethodInfo);
								dictionary.Add(existingPropertyDescriptor.Name, new PropertyLink
																					{
																						PropertyDescriptor = existingPropertyDescriptor,
																						PropertyInfo = propertyInfo
																					});
							}
							existingPropertyDescriptor.ValueRules.Merge(basePropertyDescriptor.ValueRules);
							existingPropertyDescriptor.StateRules.Merge(basePropertyDescriptor.StateRules);
						}
					}
				}
			}
		}

		private static Dictionary<PropertyInfo, List<PropertyInfo>> GetInterfaceMapping(Type type)
		{
			var dictionary = new Dictionary<PropertyInfo, List<PropertyInfo>>();
			var declaredPropertyDictionary = new Dictionary<string, PropertyInfo>();
			var bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
			var properties = type.GetProperties(bindingFlags);
			for (var index = 0; index < properties.Length; index++)
			{
				var propertyInfo = properties[index];
				declaredPropertyDictionary.Add(propertyInfo.Name, propertyInfo);
				dictionary.Add(propertyInfo, new List<PropertyInfo>());
			}

			var basePropertyNameList = GetBasePropertyList(type);

			var interfaces = type.GetInterfaces();
			for (var interfaceIndex = 0; interfaceIndex < interfaces.Length; interfaceIndex++)
			{
				var interfaceType = interfaces[interfaceIndex];
				var interfaceProperties = interfaceType.GetProperties();
				for (var interfacePropertyIndex = 0; interfacePropertyIndex < interfaceProperties.Length; interfacePropertyIndex++)
				{
					var interfaceProperty = interfaceProperties[interfacePropertyIndex];
					//try explicit first
					var explicitName = string.Format("{0}.{1}", interfaceType.FullName.Replace('+', '.'), interfaceProperty.Name);
					if (!basePropertyNameList.Contains(explicitName) && declaredPropertyDictionary.ContainsKey(explicitName))
					{
						dictionary[declaredPropertyDictionary[explicitName]].Add(interfaceProperty);
					}
					else if (!basePropertyNameList.Contains(interfaceProperty.Name) && declaredPropertyDictionary.ContainsKey(interfaceProperty.Name))
					{
						dictionary[declaredPropertyDictionary[interfaceProperty.Name]].Add(interfaceProperty);
					}
				}
			}
			return dictionary;
		}

		private static List<string> GetBasePropertyList(Type type)
		{
			var bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic;
			var list = new List<string>();
			var baseType = type.BaseType;
			while ((baseType != null) && baseType.Assembly.ContainsAttribute<IncludeAssemblyAttribute>())
			{
				var currentTypePropertyInfoList = baseType.GetProperties(bindingFlags);
				for (var index = 0; index < currentTypePropertyInfoList.Length; index++)
				{
					var propertyInfo = currentTypePropertyInfoList[index];
					if (!list.Contains(propertyInfo.Name))
					{
						list.Add(propertyInfo.Name);
					}
				}
				baseType = baseType.BaseType;
			}
			return list;
		}


		// --- Properties

		/// <summary>
		/// Gets the <see cref="System.RuntimeTypeHandle"/> for the <see cref="Type"/> this <see cref="TypeDescriptor"/> wraps.
		/// </summary>
		public RuntimeTypeHandle RuntimeTypeHandle
		{
			get;
			private set;
		}

		/// <summary>
		/// Get all the <see cref="PropertyDescriptor"/>s for this <see cref="TypeDescriptor"/>.
		/// </summary>
		public PropertyCollection Properties
		{
			get;
			private set;
		}

		/// <summary>
		/// Get all the PropertyNames
		/// </summary>
		public string[] PropertyNames
		{
			get
			{
				return this.Properties.Select(x => x.Name).ToArray();
			}
		}
		// --- Methods

#if(!WindowsCE)
		/// <summary>
		/// Get or create a <see cref="PropertyDescriptor"/> for this <see cref="TypeDescriptor"/>
		/// </summary>
		/// <remarks>If the <see cref="PropertyDescriptor"/> exists in <see cref="Properties"/> it will be returned; otherwise a new <see cref="PropertyDescriptor"/> will be created, added to <see cref="Properties"/>, and returned.</remarks>
		/// <param name="expression">The <see cref="Expression{TDelegate}"/> representing the property to get or create.</param>
		/// <returns>The existing <see cref="PropertyDescriptor"/>, if it exists in <see cref="Properties"/>; otherwise a new <see cref="PropertyDescriptor"/>, that has been created and added to <see cref="Properties"/>.</returns>
		/// <exception cref="ArgumentNullException"><paramref name="expression"/> is null.</exception>
		public PropertyDescriptor GetOrCreatePropertyDescriptor<TProperty>(Expression<Func<TProperty>> expression)
		{
			return GetOrCreatePropertyDescriptor(TypeExtensions.GetMemberName(expression));
		}
#endif
		/// <summary>
		/// Get or create a <see cref="PropertyDescriptor"/> for this <see cref="TypeDescriptor"/>
		/// </summary>
		/// <remarks>If the <see cref="PropertyDescriptor"/> exists in <see cref="Properties"/> it will be returned; otherwise a new <see cref="PropertyDescriptor"/> will be created, added to <see cref="Properties"/>, and returned.</remarks>
		/// <param name="name">The name of the <see cref="PropertyDescriptor"/> to get or create.</param>
		/// <returns>The existing <see cref="PropertyDescriptor"/>, if it exists in <see cref="Properties"/>; otherwise a new <see cref="PropertyDescriptor"/>, that has been created and added to <see cref="Properties"/>.</returns>
		/// <exception cref="ArgumentNullException"><paramref name="name"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="name"/> is <see cref="string.Empty"/>.</exception>
		/// <exception cref="ArgumentException">Property with <paramref name="name"/> has no get method.</exception>
		public PropertyDescriptor GetOrCreatePropertyDescriptor(string name)
		{
			Guard.ArgumentNotNullOrEmptyString(name, "name");
			PropertyDescriptor propertyDescriptor;
			if (!Properties.TryGetValue(name, out propertyDescriptor))
			{
				var typeFromHandle = Type.GetTypeFromHandle(RuntimeTypeHandle);
				var propertyInfo = typeFromHandle.GetProperty(name, PropertyFlags);
				if (propertyInfo == null)
				{
					var canReflectOverAssembly = AssemblyCache.CanReflectOverAssembly(typeFromHandle.Assembly, typeFromHandle.BaseType.Assembly);
					if (typeFromHandle.BaseType != null && canReflectOverAssembly)
					{
						var baseTypeDescriptor = TypeCache.GetType(typeFromHandle.BaseType.TypeHandle);
						propertyDescriptor = baseTypeDescriptor.GetOrCreatePropertyDescriptor(name);
					}
					else
					{
						throw new InvalidOperationException(string.Format("Could not find property '{0}' on type '{1}'", name, typeFromHandle.ToUserFriendlyString()));
					}
				}
				else
				{
					var getMethodInfo = propertyInfo.GetGetMethod(true);
					if (getMethodInfo == null)
					{
						throw new ArgumentException("Property must have a get. Private gets are allowed.", "name");
					}
					propertyDescriptor = new PropertyDescriptor(this, propertyInfo, getMethodInfo);
				}
				Properties.InternalAdd(propertyDescriptor);
			}
			return propertyDescriptor;
		}
#if(!WindowsCE)
		/// <summary>
		/// Remove a <see cref="PropertyDescriptor"/> from <see cref="Properties"/>.
		/// </summary>
		/// <returns>true if the <see cref="PropertyDescriptor"/> is successfully found and removed; otherwise, false.</returns>
		/// <param name="expression">The <see cref="Expression{TDelegate}"/> representing the property to remove.</param>
		/// <exception cref="ArgumentNullException"><paramref name="expression"/> is null.</exception>
		public bool RemovePropertyDescriptor<TProperty>(Expression<Func<TProperty>> expression)
		{
			return RemovePropertyDescriptor(TypeExtensions.GetMemberName(expression));
		}
#endif
		/// <summary>
		/// Remove a <see cref="PropertyDescriptor"/> from <see cref="Properties"/>.
		/// </summary>
		/// <returns>true if the <see cref="PropertyDescriptor"/> is successfully found and removed; otherwise, false.</returns>
		/// <param name="name">The name of the <see cref="PropertyDescriptor"/> to remove.</param>
		/// <exception cref="ArgumentNullException"><paramref name="name"/> is null.</exception>
		/// <exception cref="ArgumentException"><paramref name="name"/> is <see cref="string.Empty"/>.</exception>
		public bool RemovePropertyDescriptor(string name)
		{
			Guard.ArgumentNotNullOrEmptyString(name, "name");
			return Properties.InternalRemove(name);
		}

		/// <summary>
		/// Remove a <see cref="PropertyDescriptor"/> from <see cref="Properties"/>.
		/// </summary>
		/// <returns>true if the <see cref="PropertyDescriptor"/> is successfully found and removed; otherwise, false.</returns>
		/// <param name="propertyDescriptor">The <see cref="PropertyDescriptor"/> to remove.</param>
		/// <exception cref="ArgumentNullException"><paramref name="propertyDescriptor"/> is null.</exception>
		public bool RemovePropertyDescriptor(PropertyDescriptor propertyDescriptor)
		{
			return Properties.InternalRemove(propertyDescriptor);
		}

		/// <summary>
		/// Gets the total number of rules, for all sets, for this <see cref="TypeDescriptor"/>
		/// </summary>
		/// <returns></returns>
		public int GetRuleCount()
		{
			return Properties.Select(x => x.ValueRules.Count).Sum() + Properties.Select(x => x.StateRules.Count).Sum();
		}


		// --- IPropertyRuleRepository Members

		/// <summary>
		/// Return all property state rules for this type
		/// </summary>
		/// <returns></returns>
		public IList<PropertyStateRule> GetPropertyStateRules()
		{
			var rules = new List<PropertyStateRule>();
			foreach (var property in this.Properties)
			{
				rules.AddRange(property.StateRules);
			}
			return rules;
		}

		/// <summary>
		/// Return all property value rules for this type
		/// </summary>
		public IList<PropertyValueRule> GetPropertyValueRules()
		{
			var rules = new List<PropertyValueRule>();
			foreach (var property in this.Properties)
			{
				rules.AddRange(property.ValueRules);
			}
			return rules;
		}

		/// <summary>
		/// Gets names for all cached properties.
		/// </summary>
		/// <returns></returns>
		public string[] GetPropertyNames()
		{
			return this.PropertyNames;
		}
	}
}